using System;
using System.Diagnostics;
#if NETFX_CORE
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Automation.Provider;
using Windows.System;
#else
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Input;
#endif

namespace FreshMeat.Xaml
{
   /// <summary>
   /// An attached behavior which causes the specified button to be clicked when the "ESC" key is pressed.
   /// </summary>
   /// <remarks>
   /// For example, imagine a window with two sets of controls, each within their own Border (or
   /// StackPanel or whatever).  Each set of controls includes its own button that should be "clicked"
   /// when the "Enter" key is pressed while focus is in that set of controls.  To implement this,
   /// simply give each button a name, then add the following to the Border element:
   ///    CancelButtonBehavior.Button="{Binding ElementName=MyButton}"
   /// When focus is within the Border element, pressing "Enter" will cause that button to be clicked.
   /// Do the same for the other Border and its own button.  Of course, in many cases, there will
   /// be only a single scope for a window, so you can simply apply this to the Window element itself.
   /// </remarks>
   public class CancelButtonBehavior : AttachedBehavior<CancelButtonBehavior, FrameworkElement>
   {
      /// <summary>
      /// Indicates the button to be clicked when "ESC" is pressed.
      /// </summary>
      public readonly static DependencyProperty ButtonProperty = DependencyProperty.RegisterAttached(
         "Button",
         typeof(Button),
         typeof(CancelButtonBehavior),
         // ReSharper disable RedundantCast
         // Cast of null is critical -- WPF doesn't work without it!
         new FrameworkPropertyMetadata((Button)null, (d, e) => GetInstance(d)));
      // ReSharper restore RedundantCast

      public static Button GetButton(DependencyObject d)
      {
         return (Button)d.GetValue(ButtonProperty);
      }

      public static void SetButton(DependencyObject d, Button button)
      {
         d.SetValue(ButtonProperty, button);
      }

      protected override void OnAttached()
      {
         var associatedObject = TryGetAssociatedObject();
         associatedObject.KeyDown += OnAssociatedObjectKeyDown;
      }

      protected override void OnDetaching()
      {
         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
            return;

         associatedObject.KeyDown -= OnAssociatedObjectKeyDown;
      }

#if NETFX_CORE
      void OnAssociatedObjectKeyDown(object sender, KeyRoutedEventArgs e)
#else
      void OnAssociatedObjectKeyDown(object sender, KeyEventArgs e)
#endif
      {
         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
            return;

         var button = GetButton(associatedObject);
         if (button == null)
            return;

#if NETFX_CORE
         if (e.Key != VirtualKey.Escape)
#else
         if (e.Key != Key.Escape)
#endif
            return;

         if (!FocusHelper.IsFocusWithin(associatedObject))
            return;

         // First set the focus to the button to ensure whoever currently has the focus loses it.
         // This is important because controls such as the TextBox will update their binding when
         // they lose focus, which we want them do to *before* carrying out whatever operation
         // is triggered by clicking the default button.  If this wasn't done and the user had
         // just finished typing into a TextBox, whatever they typed would not be reflected in
         // the model the TextBox is bound to, so the operation would proceed without that data.
         // Neither WPF nor Silverlight perform this crucial step in their implementations of 
         // the Button.IsDefault property, making it pretty much unusable.
         Trace.TraceInformation("CancelButtonBehavior is temporarily moving focus to the button.");
         var focusContext = FocusHelper.SetTemporaryFocus(button);

         var buttonPeer = new ButtonAutomationPeer(button);
#if SILVERLIGHT
         // If we immediately click the button then the control that currently has the focus doesn't
         // realize it lost the focus and doesn't update its binding (see above).  The workaround
         // is to delay the click by invoking it via the dispatcher.
         buttonPeer.Dispatcher.BeginInvoke(() => ClickItThenRestoreFocus(buttonPeer, focusContext));
#else
         ClickItThenRestoreFocus(buttonPeer, focusContext);
#endif
      }

#if NETFX_CORE
      static async void ClickItThenRestoreFocus(AutomationPeer buttonPeer, FocusContext focusContext)
#else
      static void ClickItThenRestoreFocus(AutomationPeer buttonPeer, FocusContext focusContext)
#endif
      {
         // If the moving of the focus (see above) results in validation (or more specifically, 
         // invalidation) then the button may become disabled as a result, so we check it here.
         if (buttonPeer.IsEnabled())
         {
            Trace.TraceInformation("CancelButtonBehavior is clicking the button.");
            var provider = buttonPeer.GetPattern(PatternInterface.Invoke) as IInvokeProvider;
            if (provider != null)
               provider.Invoke();
         }

         Action restoreFocus = () =>
                               {
                                  Trace.TraceInformation("CancelButtonBehavior is restoring the focus.");
                                  focusContext.Restore();
                               };
#if NETFX_CORE
         await buttonPeer.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(restoreFocus));
#else
         buttonPeer.Dispatcher.BeginInvoke(restoreFocus);
#endif
      }
   }
}